/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import List from '@ohos.util.List';
import { Validate } from '../helper/Validate';
import { Character } from '../util/Character';
import Stack from '@ohos.util.Stack';
import { ArrayUtils } from '../util/ArrayUtils';
import hilog from '@ohos.hilog';
import uri from '@ohos.uri';
import url from '@ohos.url';

export class StringUtil {
  static readonly paddingStr: string[] = ["", " ", "  ", "   ", "    ", "     ", "      ", "       ", "        ",
    "         ", "          ", "           ", "            ", "             ", "              ", "               ",
    "                ", "                 ", "                  ", "                   ", "                    "]

  /**
   * Join a collection of strings by a separator
   * @param strings collection of string objects
   * @param sep string to place between strings
   * @return joined string
   */
  public static join(strings: Set<any> | List<any> | any[], sep: string): string {
    let size = strings instanceof Set ? strings.size : strings.length
    if (size === 0) {
      return ""
    }
    let j = new StringJoiner(sep)
    strings.forEach((str) => {
      hilog.info(0x0000, 'testTag', '%{public}s', `str:${str}`);
      j.add(str)
    })
    return j.complete()
  }

  /**
   * Returns space padding (up to the default max of 30). Use {@link #padding(int, int)} to specify a different limit.
   * @param width amount of padding desired
   * @return string of spaces * width
   * @see #padding(int, int)
   */
  public static padding(width:number,maxPaddingWidth:number = 30):string {
    Validate.isTrue(width >= 0, "width must be >= 0")
    Validate.isTrue(maxPaddingWidth >= -1)
    if (maxPaddingWidth !== -1) {
      width = Math.min(width, maxPaddingWidth)
    }
    if (width < StringUtil.paddingStr.length) {
      return StringUtil.paddingStr[width]
    }
    let outStr: string[] = []
    for (let index = 0; index < width; index++) {
      outStr[index] = ' '
    }
    return outStr.join('')
  }

  /**
   * Tests if a string is blank: null, empty, or only whitespace (" ", \r\n, \t, etc)
   * @param string string to test
   * @return if string is blank
   */
  public static isBlank(str: string): boolean {
    if (str === undefined || str === null || str.length === 0) {
      return true
    }
    let l = str.length
    for (let i = 0; i < l; i++) {
      if (!StringUtil.isWhitespace(str.codePointAt(i))) {
        return false
      }
    }
    return true;
  }

  /**
   Tests if a string starts with a newline character
   @param string string to test
   @return if its first character is a newline
   */
  public static startsWithNewline(str: string): boolean {
    if (str === undefined || str === null || str.length === 0) {
      return false
    }
    return str.charAt(0) === '\n'
  }

  /**
   * Tests if a string is numeric, i.e. contains only digit characters
   * @param string string to test
   * @return true if only digit chars, false if empty or null or contains non-digit chars
   */
  public static isNumeric(str: string): boolean {
    if (str === undefined || str === null || str.length === 0) {
      return false
    }
    let l = str.length
    for (let i = 0; i < l; i++) {
      if (!Character.isDigit(str.charAt(i))) {
        return false
      }
    }
    return true;
  }

  /**
   * Tests if a code point is "whitespace" as defined in the HTML spec. Used for output HTML.
   * @param c code point to test
   * @return true if code point is whitespace, false otherwise
   * @see #isActuallyWhitespace(int)
   */
  public static isWhitespace(c: number|string): boolean {
    if (typeof c === 'number') {
      return c === ' '.charCodeAt(0) ||
      c === '\t'.charCodeAt(0) ||
      c === '\n'.charCodeAt(0) ||
      c === '\f'.charCodeAt(0) ||
      c === '\r'.charCodeAt(0)
    } else {
      return c === ' ' ||
      c === '\t' ||
      c === '\n' ||
      c === '\f' ||
      c === '\r'
    }
  }

  /**
   * Tests if a code point is "whitespace" as defined by what it looks like. Used for Element.text etc.
   * @param c code point to test
   * @return true if code point is whitespace, false otherwise
   */
  public static isActuallyWhitespace(c: number): boolean {
    return c === ' '.charCodeAt(0) ||
    c === '\t'.charCodeAt(0) ||
    c === '\n'.charCodeAt(0) ||
    c === '\f'.charCodeAt(0) ||
    c === '\r'.charCodeAt(0) ||
    c === 160
    // 160 is &nbsp; (non-breaking space). Not in the spec but expected.
  }

  public static isInvisibleChar(c: number): boolean {
    return c === 8203 || c === 173; // zero width sp, soft hyphen
    // previously also included zw non join, zw join - but removing those breaks semantic meaning of text
  }

  /**
   * Normalise the whitespace within this string; multiple spaces collapse to a single, and all whitespace characters
   * (e.g. newline, tab) convert to a simple space.
   * @param string content to normalise
   * @return normalised string
   */
  public static normaliseWhitespace(str: string): string {
    let sb = StringUtil.borrowBuilder();
    StringUtil.appendNormalisedWhitespace(sb, str, false);
    return StringUtil.releaseBuilder(sb);
  }

  /**
   * After normalizing the whitespace within a string, appends it to a string builder.
   * @param accum builder to append to
   * @param string string to normalize whitespace within
   * @param stripLeading set to true if you wish to remove any leading whitespace
   */
  public static appendNormalisedWhitespace(accum: string[], str: string, stripLeading: boolean) {
    let lastWasWhite = false
    let reachedNonWhite = false
    let len = str.length
    let c: number
    for (let i = 0; i < len; i++) {
      c = str.charCodeAt(i)
      if (StringUtil.isActuallyWhitespace(c)) {
        if ((stripLeading && !reachedNonWhite) || lastWasWhite) {
          continue
        }
        accum.push(' ')
        lastWasWhite = true
      } else if (!StringUtil.isInvisibleChar(c)) {
        accum.push(str.charAt(i));
        lastWasWhite = false
        reachedNonWhite = true
      }
    }
  }

  public static in(needle: string, ...haystack: string[]): boolean {
    let len = haystack.length
    for (let index = 0; index < len; index++) {
      if (haystack[index] === needle) {
        return true
      }
    }
    return false
  }

  public static inSorted(needle: string, haystack: string[]): boolean {
    // return haystack.includes(needle);
    return ArrayUtils.binarySearch(haystack,needle) >= 0
  }

  /**
   Tests that a String contains only ASCII characters.
   @param string scanned string
   @return true if all characters are in range 0 - 127
   */
  public static isAscii(str: string): boolean {
    Validate.notNull(str)
    for (let i = 0; i < str.length; i++) {
      let c = str.charCodeAt(i);
      if (c > 127) { // ascii range
        return false;
      }
    }
    return true;
  }

  /**
   * Create a new absolute URL, from a provided existing absolute URL and a relative URL component.
   * @param base the existing absolute base URL
   * @param relUrl the relative URL to resolve. (If it's already absolute, it will be returned)
   * @return the resolved absolute URL
   * @throws MalformedURLException if an error occurred generating the URL
   */
  public static resolve(baseUrl: string, relUrl: string): string {
    baseUrl = StringUtil.stripControlChars(baseUrl);
    relUrl = StringUtil.stripControlChars(relUrl);
    try {
      let a = url.URL.parseURL(relUrl, baseUrl)
      return a.toString()
    } catch (e) {
      try {
        let a = url.URL.parseURL(relUrl)
        return a.toString()
      }catch (e){}
    }
    return ""
  }

  private static aaa(baseUrl: string[], relUrl: string) {
    if (relUrl.startsWith('../')) {
      relUrl = relUrl.replace('../','')
      if (baseUrl.length > 1) {
        baseUrl.pop()
      }
      if (relUrl.startsWith('../')) {
        StringUtil.aaa(baseUrl,relUrl)
      }
    }
  }

  private static stripControlChars(input:string):string {
    let controlChars = /[\x00-\x1f]*/g
    return input.replace(controlChars,"")
  }

  private static _builders:Stack<Array<string>> = new Stack();
  /**
   * Maintains cached StringBuilders in a flyweight pattern, to minimize new StringBuilder GCs. The StringBuilder is
   * prevented from growing too large.
   * <p>
   * Care must be taken to release the builder once its work has been completed, with {@link #releaseBuilder}
   * @return an empty StringBuilder
   */
  public static borrowBuilder(): string[] {
    if (this._builders.isEmpty) {
      return new Array();
    } else {
      return this._builders.pop();
    }
  }

  /**
   * Release a borrowed builder. Care must be taken not to use the builder after it has been returned, as its
   * contents may be changed by this method, or by a concurrent thread.
   * @param sb the StringBuilder to release.
   * @return the string value of the released String Builder (as an incentive to release it!).
   */
  public static releaseBuilder(sb:string[]) :string {
    Validate.notNull(sb)
    let result = sb.join('')
    sb.length = 0
    this._builders.push(sb)
    return result
  }

  public static regionMatches(ignoreCase:boolean, src:string,toffset:number,other:string,ooffset:number,len:number):boolean {
    if (toffset < 0 || ooffset < 0 || toffset + len > src.length || ooffset + len > other.length) {
      return false
    }
    let str1 = src.substring(toffset, toffset + len)
    let str2 = other.substring(ooffset,ooffset+len)
    return ignoreCase?str1.toLowerCase() === str2.toLowerCase():str1 === str2
  }

}


/**
 A StringJoiner allows incremental / filtered joining of a set of stringable objects.
 @since 1.14.1
 */
export class StringJoiner {
  // sets null on builder release so can't accidentally be reused
  sb = StringUtil.borrowBuilder()
  readonly separator: string
  first = true
  /**
   Create a new joiner, that uses the specified separator. MUST call {@link #complete()} or will leak a thread
   local string builder.

   @param separator the token to insert between strings
   */
  constructor(separator: string) {
    this.separator = separator
  }

  /**
   Add another item to the joiner, will be separated
   */
  public add(stringy: any): StringJoiner {
    // don't reuse
    Validate.notNull(this.sb)
    if (!this.first) {
      this.sb.push(this.separator)
    }
    this.sb.push(stringy)
    this.first = false
    return this
  }

  /**
   Append content to the current item; not separated
   */
  public append(stringy: any): StringJoiner {
    // don't reuse
    Validate.notNull(this.sb)
    this.sb.push(stringy)
    return this
  }
  /**
   Return the joined string, and release the builder back to the pool. This joiner cannot be reused.
   */
  public complete(): string {
    let result = StringUtil.releaseBuilder(this.sb)
    this.sb = null
    return result
  }
}